<?php
// vim: set ts=4 sw=4 sts=4 et:

/**
 * LiteCommerce
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Open Software License (OSL 3.0)
 * that is bundled with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://opensource.org/licenses/osl-3.0.php
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to licensing@litecommerce.com so we can send you a copy immediately.
 *
 * PHP version 5.3.0
 *
 * @category  LiteCommerce
 * @author    Creative Development LLC <info@cdev.ru>
 * @copyright Copyright (c) 2011 Creative Development LLC <info@cdev.ru>. All rights reserved
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
 * @link      http://www.litecommerce.com/
 * @see       ____file_see____
 * @since     1.0.0
 */

namespace XLite\Core;

/**
 * XML parser
 *
 * @see   ____class_see____
 * @since 1.0.0
 */
class XML extends \XLite\Base
{
    /**
     * Get formatted XML block
     *
     * @param string $xml XML
     *
     * @return string
     * @see    ____func_see____
     * @since  1.0.0
     */
    public static function getFormattedXML($xml)
    {
        $xml = preg_replace('/>[ ' . "\t\n\r" . ']+</', '><', trim($xml));

        $indentStr = ' ';
        $level = -1;
        $i = 0;
        $prev = 0;
        $path = array();
        while (preg_match('/<([\w\d_\?]+)(?: [^>]+)?' . '>/S', substr($xml, $i), $match)) {
            $tn = $match[1];
            $len = strlen($match[0]);
            $i = strpos($xml, $match[0], $i);
            $level++;

            // Detect close-tags
            if (0 < $i - $prev) {
                $ends = substr_count(substr($xml, $prev, $i - $prev), '</');
                if (0 < $ends) {
                    $level -= $ends;
                }
            }

            // Add indents
            if (0 < $level) {
                $xml = substr($xml, 0, $i) . str_repeat($indentStr, $level) . substr($xml, $i);
                $i += $level;
            }

            // Add EOL symbol
            $end = strpos(substr($xml, $i + $len), '</' . $tn . '>');
            if (
                (false !== $end && preg_match('/<[\w\d_\?]+(?: [^>]+)?' . '>/S', substr($xml, $i + $len, $end)))
                || '?' == substr($tn, 0, 1)
            ) {
                $xml = substr($xml, 0, $i + $len) . "\n" . substr($xml, $i + $len);
                $i++;

                // Add indent for close-tag
                if (0 < $level) {
                    $end += $i + $len;
                    $xml = substr($xml, 0, $end) . str_repeat($indentStr, $level) . substr($xml, $end);
                }
            }

            $i += $len;
            $prev = $i;
        }

        return preg_replace('/(<\/[\w\d_]+>)/', '\1' . "\n", $xml);
    }

    /**
     * Parses XML data into array with attributes 
     * 
     * @param string $data    XML string
     * @param string &$error  Error message (will be returned by reference)
     * @param array  $options Array of XML parser options OPTIONAL
     *  
     * @return array
     * @see    ____func_see____
     * @since  1.0.0
     */
    public function parse($data, &$error, $options = array())
    {
        static $defaultOptions = array (
            'XML_OPTION_CASE_FOLDING' => 0,
            'XML_OPTION_SKIP_WHITE' => 1
        );

        $data = trim($data);
        $vals = $index = $array = array();
        $parser = xml_parser_create();
        $options = array_merge($defaultOptions, $options);

        foreach ($options as $opt=>$val) {
            if (defined($opt)) {
                xml_parser_set_option($parser, constant($opt), $val);
            }
        }

        if (!xml_parse_into_struct($parser, $data, $vals, $index)) {
            $error = array (
                'code' => xml_get_error_code($parser),
                'string' => xml_error_string(xml_get_error_code($parser)),
                'line' => xml_get_current_line_number($parser)
            );
            xml_parser_free($parser);
            $array = false;
        
        } else {

            xml_parser_free($parser);

            $i = 0;

            $tagname = $vals[$i]['tag'];

            if (isset($vals[$i]['attributes'])) {
                $array[$tagname]['@'] = $vals[$i]['attributes'];
            
            } else {
                $array[$tagname]['@'] = array();
            }

            $array[$tagname]['#'] = $this->makeTree($vals, $i);
        }

        return $array;
    }

    /**
     * Returns element of array by path to it or false when $tagPath cannot be resolved
     * 
     * @param array   &$array  Array which was generated by parse() method
     * @param string  $tagPath Tag path within XML parsed
     * @param boolean $strict  Flag: true if tag_path should be strictly compliant OPTIONAL
     *  
     * @return mixed
     * @see    ____func_see____
     * @since  1.0.0
     */
    public function getArrayByPath(&$array, $tagPath, $strict = false)
    {
        if (is_array($array) && !empty($array)) {

            if (!empty($tagPath)) {
 
                $path = explode('/', $tagPath);

                $elem = & $array;

                foreach ($path as $key) {

                    if (isset($elem[$key])) {
                        $tmpElem = & $elem[$key];

                    } else {

                        if (!$strict && isset($elem['#'][$key])) {
                            $tmpElem = & $elem['#'][$key];

                        } elseif (!$strict && isset($elem[0]['#'][$key])) {
                            $tmpElem = & $elem[0]['#'][$key];

                        } else {
                            // path is not found
                            $elem = false;
                            break;
                        }
                    }

                    unset($elem);
                    $elem = $tmpElem;
                    unset($tmpElem);
                }

            } else {
                $elem = $array;
            }

        } else {
            $elem = false;
        }

        return $elem;
    }

    /**
     * Recursively builds a tree from parsed XML
     * 
     * @param array   $vals Array of values
     * @param integer &$i   Level
     *  
     * @return array
     * @see    ____func_see____
     * @since  1.0.0
     */
    protected function makeTree($vals, &$i)
    {
        $children = array();

        if (isset($vals[$i]['value'])) {
            array_push($children, $vals[$i]['value']);
        }

        while (count($vals) > ++$i) {
            
            switch ($vals[$i]['type']) {

                case 'open':
                    
                    if (isset($vals[$i]['tag'])) {
                        $tagname = $vals[$i]['tag'];
                    
                    } else {
                        $tagname = '';
                    }

                    if (isset($children[$tagname])) {
                        $size = sizeof($children[$tagname]);
                    } else {
                        $size = 0;
                    }

                    if (isset($vals[$i]['attributes'])) {
                        $children[$tagname][$size]['@'] = $vals[$i]['attributes'];
                    }

                    $children[$tagname][$size]['#'] = $this->makeTree($vals, $i);
                    break;

                case 'cdata':
                    array_push($children, $vals[$i]['value']);
                    break;

                case 'complete':
                    $tagname = $vals[$i]['tag'];

                    if (isset($children[$tagname])) {
                        $size = sizeof($children[$tagname]);
                    
                    } else {
                        $size = 0;
                    }

                    if (isset($vals[$i]['value'])) {
                        $children[$tagname][$size]['#'] = $vals[$i]['value'];
                    
                    } else {
                        $children[$tagname][$size]['#'] = '';
                    }

                    if (isset($vals[$i]['attributes'])) {
                        $children[$tagname][$size]['@'] = $vals[$i]['attributes'];
                    }

                    break;

                case 'close':
                    return $children;
                    break;

                default:
            }
        }

        return $children;
    }
}
